package com.suse.service.impl;

import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSONObject;
import com.suse.common.config.redis.RedisData;
import com.suse.common.config.redis.RedisClient;
import com.suse.common.util.core.BeanUtil;
import com.suse.common.util.time.LocalDateTimeUtil;
import com.suse.constant.RedisConstant;
import com.suse.entity.Shop;
import com.suse.mapper.ShopMapper;
import com.suse.result.Result;
import com.suse.service.ShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.suse.common.util.core.StringUtil;
import com.suse.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static com.suse.result.ResultEnum.RESULT_SHOP_NOT_FOUND;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements ShopService {

    // 锁
    public static ReentrantLock lock = new ReentrantLock();

    @Resource
    private RedisClient redisClient;

    @Resource
    private Executor executor;

    @Override
    public Result getShop(long id) {
        return getShopWithLogicExpire(id);
    }

    /**
     * 缓存空值解决商户缓存穿透问题
     * @param id 商户ID
     * @return 结果
     */
    public Result getShopWithNull(long id) {
        // 1.从缓存中查询数据
        String shopJson = getShopInCache(id);
        // 2.判断是否缓存是否有数据
        if (StringUtil.isNotNull(shopJson)) {
            // 3.有数据直接返回
            return Result.success(JSONObject.parseObject(shopJson, RedisData.class));
        }
        // 4.没有数据从数据库中查询数据并重建缓存
        Shop shop = getShopInDB(id);
        if (ObjectUtil.isNull(shop)) {
            return Result.error(RESULT_SHOP_NOT_FOUND);
        }
        // 5.返回数据
        return Result.success(shop);
    }

    /**
     * Redis实现自旋锁解决商户缓存击穿问题
     * @param id 商户ID
     * @return 结果
     */
    public Result getShopWithSpinLock(long id) {
        do {
            // 1.查看缓存中是否有数据
            String shopJson = getShopInCache(id);
            if (StringUtil.isNotNull(shopJson)) {
                // 2.有数据直接返回
                return Result.success(JSONObject.parseObject(shopJson, RedisData.class));
            }
            // 3.没有数据尝试获取锁，获取不到锁就一直自旋，解决缓存击穿问题
        }while (!redisClient.trylock(RedisUtil.getShopLockKey(id)));
        // 4.获取锁成功，再次查看缓存中是否有数据（Double Check）
        return getShopWithNull(id);
    }

    /**
     * 可重入锁解决商户缓存击穿问题
     * @param id 商户ID
     * @return 结果
     */
    public Result getShopWithReentrantLock(long id) {
        // 1.查看缓存中是否有数据
        String shopJson = getShopInCache(id);
        if (StringUtil.isNotNull(shopJson)) {
            // 2.有数据直接返回
            return Result.success(shopJson);
        }
        try {
            // 3.没有数据尝试获取锁，解决缓存击穿问题
            lock.lock();
            // 4.获取锁成功，再次查看缓存中是否有数据（Double Check）
            return getShopWithNull(id);
        } finally {
            // 缓存重建完成之后释放锁
            lock.unlock();
        }
    }

    /**
     * 逻辑过期解决商户缓存击穿问题
     * @param id 商户ID
     * @return 结果
     */
    public Result getShopWithLogicExpire(long id) {
        // 1.从缓存中查询数据
        String shopJson = getShopInCache(id);
        if (existAndNoLogicExpire(shopJson)) {
            // 2.数据存在并且没有逻辑过期，直接返回数据
            return Result.success(JSONObject.parseObject(shopJson, RedisData.class));
        }
        // 3.数据不存在或者已逻辑过期，尝试获取锁
        if (redisClient.trylock(RedisUtil.getShopLockKey(id))) {
            // 4.获取锁成功再次查询缓存中数据并判断是否不存在或逻辑过期（Double Check）
            shopJson = getShopInCache(id);
            if (existAndNoLogicExpire(shopJson)) {
                // 5.数据存在并且没有逻辑过期，直接返回数据
                return Result.success(JSONObject.parseObject(shopJson, RedisData.class));
            }
            // 6.开启一个线程异步重建缓存
            executor.execute(() -> rebuildCacheWithLogicExpire(id));
        }
        // 8.没有获取到锁的线程再次查看缓存（提高数据一致性） 可能返回逻辑过期的数据（旧数据）（短时间的数据不一致）
        return getShopWithSpinLock(id);
    }

    /**
     * 判断商户缓存是否存在并且没有逻辑过期
     * @param shopJson 商户缓存
     * @return 是否没有逻辑过期
     */
    private boolean existAndNoLogicExpire(String shopJson) {
        if (StringUtil.isNull(shopJson)) {
            return false;
        }
        RedisData redisData = BeanUtil.jsonToBean(shopJson, RedisData.class);
        long expireTimeStamp = redisData.getExpire();
        LocalDateTime expireDateTime = LocalDateTimeUtil.of(expireTimeStamp);
        return LocalDateTime.now().isBefore(expireDateTime);
    }

    /**
     * 从缓存中查询商户信息
     * @param id 商户ID
     * @return 结果
     */
    private String getShopInCache(long id) {
        String shopKey = RedisUtil.getShopKey(id);
        // 1.从缓存中查询数据
        String shopJson = redisClient.get(shopKey);
        if (StringUtil.isNotNull(shopJson)) {
            return shopJson;
        }
        // 2.不存在，返回null
        return null;
    }

    /**
     * 从数据库中查询商户信息并重建缓存
     * @param id 商户ID
     * @return 商户信息
     */
    private Shop getShopInDB(long id) {
        Shop shop = getById(id);
        String shopKey = RedisUtil.getShopKey(id);
        // 7.判断数据是否存在
        if (shop == null) {
            // 8.不存在，缓存空值并设置过期时间，解决缓存穿透问题
            redisClient.setn(shopKey);
            // 9.返回null
            return null;
        }
        doRebuildCache(shop,false);
        return shop;
    }

    private void rebuildCacheWithLogicExpire(long id) {
        Shop shop = getById(id);
        String shopKey = RedisUtil.getShopKey(id);
        // 7.判断数据是否存在
        if (shop == null) {
            // 8.不存在，缓存空值并设置过期时间，解决缓存穿透问题
            redisClient.setn(shopKey);
            // 9.返回
            return;
        }
        doRebuildCache(shop,true);
    }

    private void doRebuildCache(Shop shop,boolean logicExpire) {
        Long id = shop.getId();
        String shopKey = RedisUtil.getShopKey(id);
        // 模拟复杂缓存重建的延时
        ThreadUtil.sleep(200);
        if (logicExpire) {
            // 重建缓存中并设置逻辑过期时间 30 s
            redisClient.setlx(shopKey,shop, RedisConstant.SHOP_TTL, TimeUnit.MINUTES);
            // 注意：一定要在异步线程重建缓存完之后释放锁！！！
            redisClient.del(RedisUtil.getShopLockKey(id));
        }else {
            // 重建缓存中并设置过期时间
            redisClient.set(shopKey,shop, RedisConstant.SHOP_TTL, TimeUnit.MINUTES);
        }
    }

    @Transactional
    @Override
    public Result updateShop(Shop shop) {
        // 1.更新之前判断 id 是否存在，不存在，直接返回错误信息，避免对数据库的无效访问
        if (shop.getId() == null) {
            // 2.不存在，直接返回错误信息
            return Result.error(RESULT_SHOP_NOT_FOUND);
        }
        // 3.存在，先更新数据库数据
        updateById(shop);
        // 4.再删除缓存 注意: 把对数据库和缓存的操作放在一个事务里面并且把缓存的操作放在事务的最后 缓存删除失败会抛出异常 数据库事务会回滚！！！
        String shopKey = RedisUtil.getShopKey(shop.getId());
        redisClient.del(shopKey);
        return Result.success();
    }

    @Override
    public Result buildLogicExpireShopCache(long id) {
        Shop shop = getById(id);
        doRebuildCache(shop,true);
        return Result.success();
    }

}
